When you build multithreaded applications, your program needs to ensure that any piece of shared data is protected against the possibility of numerous threads changing its value. Given that all threads in an AppDomain have concurrent access to the shared data of the application, imagine what might happen if multiple threads were accessing the same point of data. As the thread scheduler will force threads to suspend their work at random, what if thread A is kicked out of the way before it has fully completed its work? Thread B is now reading unstable data.
To illustrate the problem of concurrency, let’s build another Console Application project named MultiThreadedPrinting. This application will once again make use of the Printer class created previously, but this time the PrintNumbers() method will force the current thread to pause for a randomly generated amount of time:
public class Printer { public void PrintNumbers() { ... for (int i = 0; i < 10; i++) { // Put thread to sleep for a random amount of time. Random r = new Random(); Thread.Sleep(1000 * r.Next(5)); Console.Write("{0}, ", i); } Console.WriteLine(); } }
The Main() method is responsible for creating an array of ten (uniquely named) Thread objects, each of which is making calls on the same instance of the Printer object:
class Program { static void Main(string[] args) { Console.WriteLine("*****Synchronizing Threads *****\n"); Printer p = new Printer(); // Make 10 threads that are all pointing to the same // method on the same object. Thread[] threads = new Thread[10]; for (int i = 0; i < 10; i++) { threads[i] = new Thread(new ThreadStart(p.PrintNumbers)); threads[i].Name = string.Format("Worker thread #{0}", i); } // Now start each one. foreach (Thread t in threads) t.Start(); Console.ReadLine(); } }
Before looking at some test runs, let’s recap the problem. The primary thread within this AppDomain begins life by spawning ten secondary worker threads. Each worker thread is told to make calls on the PrintNumbers() method on the same Printer instance. Given that you have taken no precautions to lock down this object’s shared resources (the console), there is a good chance that the current thread will be kicked out of the way before the PrintNumbers() method is able to print out the complete results. Because you don’t know exactly when (or if) this might happen, you are bound to get unpredictable results. For example, you might find the output shown here:
Now run the application a few more times. Here is another possibility (your results will certainly differ).
*****Synchronizing Threads ***** -> Worker thread #1 is executing PrintNumbers() Your numbers: -> Worker thread #0 is executing PrintNumbers() -> Worker thread #2 is executing PrintNumbers() Your numbers: -> Worker thread #3 is executing PrintNumbers() Your numbers: -> Worker thread #4 is executing PrintNumbers() Your numbers: -> Worker thread #6 is executing PrintNumbers() Your numbers: -> Worker thread #7 is executing PrintNumbers() Your numbers: -> Worker thread #8 is executing PrintNumbers() Your numbers: -> Worker thread #9 is executing PrintNumbers() Your numbers: Your numbers: -> Worker thread #5 is executing PrintNumbers() Your numbers: 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 2, 1, 0, 0, 4, 3, 4, 1, 2, 4, 5, 5, 5, 6, 6, 6, 2, 7, 7, 7, 3, 4, 0, 8, 4, 5, 1, 5, 8, 8, 9, 2, 6, 1, 0, 9, 1, 6, 2, 7, 9, 2, 1, 7, 8, 3, 2, 3, 3, 9, 8, 4, 4, 5, 9, 4, 3, 5, 5, 6, 3, 6, 7, 4, 7, 6, 8, 7, 4, 8, 5, 5, 6, 6, 8, 7, 7, 9, 8, 9, 8, 9, 9, 9,
Now run the application a few more times. Here is another possibility (your results will certainly differ).
*****Synchronizing Threads ***** -> Worker thread #0 is executing PrintNumbers() -> Worker thread #1 is executing PrintNumbers() -> Worker thread #2 is executing PrintNumbers() Your numbers: -> Worker thread #4 is executing PrintNumbers() Your numbers: -> Worker thread #5 is executing PrintNumbers() Your numbers: Your numbers: -> Worker thread #6 is executing PrintNumbers() Your numbers: -> Worker thread #7 is executing PrintNumbers() Your numbers: Your numbers: -> Worker thread #8 is executing PrintNumbers() Your numbers: -> Worker thread #9 is executing PrintNumbers() Your numbers: -> Worker thread #3 is executing PrintNumbers() Your numbers: 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7 , 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
NoteIf you are unable to generate unpredictable outputs, increase the number of threads from 10 to 100 (for example) or introduce another call to Thread.Sleep() within your program. Eventually, you will encounter the concurrency issue.
There are clearly some problems here. As each thread is telling the Printer to print out the numerical data, the thread scheduler is happily swapping threads in the background. The result is inconsistent output. What you need is a way to programmatically enforce synchronized access to the shared resources. As you would guess, the System.Threading namespace provides a number of synchronization-centric types. The C# programming language also provides a particular keyword for the very task of synchronizing shared data in multithreaded applications.
The first technique you can use to synchronize access to shared resources is the C# lock keyword. This keyword allows you to define a scope of statements that must be synchronized between threads. By doing so, incoming threads cannot interrupt the current thread, preventing it from finishing its work. The lock keyword requires you to specify a token (an object reference) that must be acquired by a thread to enter within the lock scope. When you are attempting to lock down a private instance-level method, you can simply pass in a reference to the current type:
private void SomePrivateMethod() { // Use the current object as the thread token. lock(this) { // All code within this scope is thread-safe. } }
However, if you are locking down a region of code within a public member, it is safer (and a best practice) to declare a private object member variable to serve as the lock token:
public class Printer { // Lock token. private object threadLock = new object(); public void PrintNumbers() { // Use the lock token. lock (threadLock) { ... } } }
In any case, if you examine the PrintNumbers() method, you can see that the shared resource the threads are competing to gain access to is the console window. Therefore, if you scope all interactions with the Console type within a lock scope as follows:
public void PrintNumbers() { // Use the private object lock token. lock (threadLock) { // Display Thread info. Console.WriteLine("-> {0} is executing PrintNumbers()", Thread.CurrentThread.Name); // Print out numbers. Console.Write("Your numbers: "); for (int i = 0; i < 10; i++) { Random r = new Random(); Thread.Sleep(1000 * r.Next(5)); Console.Write("{0}, ", i); } Console.WriteLine(); } }
you have effectively designed a method that will allow the current thread to complete its task. Once a thread enters into a lock scope, the lock token (in this case, a reference to the current object) is inaccessible by other threads until the lock is released once the lock scope has exited. Thus, if thread A has obtained the lock token, other threads are unable to enter any scope that uses the same lock token until thread A relinquishes the lock token.
Note If you are attempting to lock down code in a static method, simply declare a private static object member variable to serve as the lock token.
If you now run the application, you can see that each thread has ample opportunity to finish its business:
*****Synchronizing Threads ***** -> Worker thread #0 is executing PrintNumbers() Your numbers: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -> Worker thread #1 is executing PrintNumbers() Your numbers: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -> Worker thread #3 is executing PrintNumbers() Your numbers: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -> Worker thread #2 is executing PrintNumbers() Your numbers: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -> Worker thread #4 is executing PrintNumbers() Your numbers: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -> Worker thread #5 is executing PrintNumbers() Your numbers: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -> Worker thread #7 is executing PrintNumbers() Your numbers: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -> Worker thread #6 is executing PrintNumbers() Your numbers: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -> Worker thread #8 is executing PrintNumbers() Your numbers: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -> Worker thread #9 is executing PrintNumbers() Your numbers: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
Source Code The MultiThreadedPrinting project is included under the Chapter 19 subdirectory.
The C# lock statement is really just a shorthand notation for working with the System.Threading.Monitor class. Once processed by the C# compiler, a lock scope actually resolves to the following (which you can verify using ildasm.exe or reflector.exe):
public void PrintNumbers() { Monitor.Enter(threadLock); try { // Display Thread info. Console.WriteLine("-> {0} is executing PrintNumbers()", Thread.CurrentThread.Name); // Print out numbers. Console.Write("Your numbers: "); for (int i = 0; i < 10; i++) { Random r = new Random(); Thread.Sleep(1000 * r.Next(5)); Console.Write("{0}, ", i); } Console.WriteLine(); } finally { Monitor.Exit(threadLock); } }
First, notice that the Monitor.Enter() method is the ultimate recipient of the thread token you specified as the argument to the lock keyword. Next, all code within a lock scope is wrapped within a try block. The corresponding finally clause ensures that the thread token is released (via the Monitor.Exit() method), regardless of any possible runtime exception. If you were to modify the MultiThreadSharedData program to make direct use of the Monitor type (as just shown), you will find the output is identical.
Now, given that the lock keyword seems to require less code than making explicit use of the System.Threading.Monitor type, you may wonder about the benefits of using the Monitor type directly. The short answer is control. If you make use of the Monitor type, you are able to instruct the active thread to wait for some duration of time (via the static Monitor.Wait() method), inform waiting threads when the current thread is completed (via the static Monitor.Pulse() and Monitor.PulseAll() methods), and so on.
As you would expect, in a great number of cases, the C# lock keyword will fit the bill. However, if you are interested in checking out additional members of the Monitor class, consult the .NET Framework 4.0 SDK documentation.
Although it always is hard to believe until you look at the underlying CIL code, assignments and simple arithmetic operations are not atomic. For this reason, the System.Threading namespace provides a type that allows you to operate on a single point of data atomically with less overhead than with the Monitor type. The Interlocked class defines the following key static members shown in Table 19-4.
Table 19-4. Select Members of the System.Threading.Interlocked Type
Member | Meaning in Life |
---|---|
CompareExchange() | Safely tests two values for equality and, if equal, changes one of the values with a third |
Decrement() | Safely decrements a value by 1 |
Exchange() | Safely swaps two values |
Increment() | Safely increments a value by 1 |
Although it might not seem like it from the onset, the process of atomically altering a single value is quite common in a multithreaded environment. Assume you have a method named AddOne() that increments an integer member variable named intVal. Rather than writing synchronization code such as the following:
public void AddOne() { lock(myLockToken) { intVal++; } }
you can simplify your code via the static Interlocked.Increment() method. Simply pass in the variable to increment by reference. Do note that the Increment() method not only adjusts the value of the incoming parameter, but also returns the new value:
public void AddOne() { int newVal = Interlocked.Increment(ref intVal); }
In addition to Increment() and Decrement(), the Interlocked type allows you to atomically assign numerical and object data. For example, if you wish to assign the value of a member variable to the value 83, you can avoid the need to use an explicit lock statement (or explicit Monitor logic) and make use of the Interlocked.Exchange() method:
public void SafeAssignment() { Interlocked.Exchange(ref myInt, 83); }
Finally, if you wish to test two values for equality and change the point of comparison in a threadsafe manner, you are able to leverage the Interlocked.CompareExchange() method as follows:
public void CompareAndExchange() { // If the value of i is currently 83, change i to 99. Interlocked.CompareExchange(ref i, 99, 83); }
The final synchronization primitive examined here is the [Synchronization] attribute, which is a member of the System.Runtime.Remoting.Contexts namespace. In essence, this class-level attribute effectively locks down all instance member code of the object for thread safety. When the CLR allocates objects attributed with [Synchronization], it will place the object within a synchronized context. As you may recall from Chapter 17, objects that should not be removed from a contextual boundary should derive from ContextBoundObject. Therefore, if you wish to make the Printer class type thread-safe (without explicitly writing thread-safe code within the class members), you could update the definition as follows:
using System.Runtime.Remoting.Contexts; ... // All methods of Printer are now thread-safe! [Synchronization] public class Printer : ContextBoundObject { public void PrintNumbers() { ... } }
In some ways, this approach can be seen as the lazy way to write thread-safe code, given that you are not required to dive into the details about which aspects of the type are truly manipulating threadsensitive data. The major downfall of this approach, however, is that even if a given method is not making use of thread-sensitive data, the CLR will still lock invocations to the method. Obviously, this could degrade the overall functionality of the type, so use this technique with care.